/**
 * 
 */
package stc.skymobi.ebus.unit;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;

import stc.skymobi.ebus.EventBus;
import stc.skymobi.util.ProxyBuilder;


/**
 * @author hp
 *
 */
public class ZipEventUnitImpl implements EventUnit {

	 private static final Logger logger = 
		 LoggerFactory.getLogger(ZipEventUnitImpl.class);
	
    private UnitSource                	source;
    private	ApplicationContext			root;
    private	ExecutorService				exec;
    
    /* This subclass of ByteArrayResource attempts to circumvent a bug in
     * org.springframework.core.io.support.PropertiesLoaderSupport.loadProperties()
     * that tries to fetch the filename extension of the properties Resource
     * in an attempt to determine whether to parse the properties as XML or
     * traditional syntax.  ByteArrayResource throws an exception when
     * getFilename() is called because there is no associated filename.
     * This subclass returns a fake filename (without a .xml extension).
     * TODO: Remove this when Spring Framework SPR-5068 gets fixed:
     * http://jira.springframework.org/browse/SPR-5068
     */
    private static class InputStreamResourceHack extends InputStreamResource {
    	public InputStreamResourceHack(InputStream inputStream) {
			super(inputStream);
		}

		public String getFilename() {
	        return "InputStreamResourceHasNoFilename";
		}
    }
    
    public ZipEventUnitImpl(
    		UnitSource 			source, 
    		ApplicationContext 	root) {
    	this.source = source;
    	this.root = root;
    }
    
    private void unzipUnit(String unzipBasePath) throws IOException {
    	final int BUFFER_SIZE = 4096;
    	
    	ZipFile	zipFile = null;
    	
    	try {
    		//	eg. c:/units/jars/rop-mis.zip

    		//	eg. mkdir c:/units/jars/rop-mis/
            new File( unzipBasePath ).mkdirs();
            
			zipFile = new ZipFile(source.getName());
			
			//	解压zip file
			Enumeration<? extends ZipEntry> emu = zipFile.entries();
            while(emu.hasMoreElements()){
                ZipEntry entry = emu.nextElement();
                //会把目录作为一个file读出一次，所以只建立目录就可以，之下的文件还会被迭代到。
                if (entry.isDirectory())
                {
                    new File( unzipBasePath + entry.getName()).mkdirs();
                    continue;
                }
                BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
                File file = new File(unzipBasePath + entry.getName());
                
                //	加入这个的原因是zipfile读取文件是随机读取的，这就造成可能先读取一个文件
                //	而这个文件所在的目录还没有出现过，所以要建出目录来。
                File parent = file.getParentFile();
                if(parent != null && (!parent.exists())){
                    parent.mkdirs();
                }
                
                FileOutputStream fos = new FileOutputStream(file);
                BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE);
                
                int count;
                byte data[] = new byte[BUFFER_SIZE];
                while ((count = bis.read(data, 0, BUFFER_SIZE)) != -1)
                {
                    bos.write(data, 0, count);
                }
                bos.flush();
                bos.close();
                bis.close();
            }
	    	
		} 
		finally {
			if ( null != zipFile ) {
	            try {
					zipFile.close();
				} catch (IOException e) {
					logger.error("failed to close zip file {}, reason {}", source.getName(), e );
				}
			}
		}
    }
    
    @SuppressWarnings("unchecked")
	private AbstractApplicationContext initTopContext(String basePath) throws Exception {
        AbstractApplicationContext top = null;
        
        if ( null != root ) {
        	top = new ClassPathXmlApplicationContext(
        		new String[]{"stc/skymobi/ebus/unit/zipUnitRoot.xml"}, root);
        }
        else {
        	top = new ClassPathXmlApplicationContext("stc/skymobi/ebus/unit/zipUnitRoot.xml");
        }
        
        String configFilename = basePath + "bin/units.properties";
        
        ProxyBuilder<Resource> resourceProxyBuilder = 
        	(ProxyBuilder<Resource>)top.getBean("resourceProxyBuilder");
        
        resourceProxyBuilder.setImpl(
        	new InputStreamResourceHack( new FileInputStream( new File(configFilename) ) )
		);

        //	setting event for units changed 
        ProxyBuilder<CharSequence> eventUnitsChangedBuilder = 
        	(ProxyBuilder<CharSequence>)top.getBean("eventUnitsChangedBuilder");
        
        eventUnitsChangedBuilder.setImpl(
        		"event." + source.getName() + ".unitsChanged." 
        		+ UUID.randomUUID().toString());
        
        //	set booter name
        ProxyBuilder<CharSequence> booterNameBuilder = 
        	(ProxyBuilder<CharSequence>)top.getBean("booterNameBuilder");
        booterNameBuilder.setImpl(source.getName());
        
        //	set base path
        ProxyBuilder<CharSequence> basePathBuilder = 
        	(ProxyBuilder<CharSequence>)top.getBean("basePathBuilder");
        basePathBuilder.setImpl( basePath );
        
        //	set base path
        ProxyBuilder<CharSequence> appMBeanPrefixBuilder = 
        	(ProxyBuilder<CharSequence>)top.getBean("appMBeanPrefixBuilder");
        appMBeanPrefixBuilder.setImpl( 
        		"app:app=" + 
        		FilenameUtils.getBaseName(source.getName()) +
        		",");
        
        return	top;
    }
    
    private AbstractApplicationContext initZipUnitContext(AbstractApplicationContext parent) 
    	throws Exception {
    	
        AbstractApplicationContext ctx = 
        	new ClassPathXmlApplicationContext(
        			new String[]{"stc/skymobi/ebus/unit/zipUnitLauncher.xml"}, parent);
    	
		UnitGroupBooter booter = (UnitGroupBooter) ctx.getBean("booter");
		booter.setRootAppCtx(ctx);
		
		FsUnitSourceGroupManager mgr = (FsUnitSourceGroupManager)ctx.getBean("fsUnitSourcesManager");
		try {
			mgr.start();
		} catch (Exception e) {
			logger.error("failed to start FsUnitSourceGroupManager, reason {}", e );
		}
		
		return ctx;
    }

    private List<URL> collectJarFilename(String basePath) throws IOException {
    	
		List<URL> ret = new ArrayList<URL>();
    	
		File dir = new File(basePath);
        if (dir.isDirectory()) {
        	File[] files = dir.listFiles(new FilenameFilter(){

				public boolean accept(File dir, String name) {
					return	name.endsWith(".jar");
				}});
        	
        	for ( File file : files) {
        		if ( file.isDirectory() ) {
        			ret.addAll( collectJarFilename( file.getCanonicalPath() ) );
        		}
        		else {
        			ret.add( file.toURI().toURL() );
        		}
        	}
        }
        
        return	ret;
    }
    
    private	ClassLoader createZipUnitClassLoad(String basePath, ClassLoader parent) {
    	
    	List<URL> allJars = null;
		try {
			allJars = collectJarFilename(basePath + "/lib/");
	    	allJars.addAll(collectJarFilename(basePath + "/bin/"));
		} catch (IOException e) {
			logger.error("failed to collectJarFilename, reason {}", e );
		}
    	
		if ( null != allJars ) {
			return new URLClassLoader(allJars.toArray(new URL[0]), parent);
		}
		else {
			return	null;
		}
    }
    
    /* (non-Javadoc)
     * @see com.skymobi.ebus.unit.EventUnit#init(com.skymobi.ebus.EventBus)
     */
//    @Override
	public Object init(EventBus eventBus) {
    	//	TODO, handle source.getInputStream 
    	Object ret = null;
   	
		final String basePath = FilenameUtils.getFullPath( source.getName() ) + 
							FilenameUtils.getBaseName( source.getName() ) + 
							"-" + Long.toString( System.currentTimeMillis() ) +
							"/";
		
		try {
			unzipUnit(basePath);
				
			exec = Executors.newSingleThreadExecutor(new ThreadFactory(){

				public Thread newThread(Runnable r) {
					return new Thread(r, source.getName() + " Launcher");
				}});
			
			Future<AbstractApplicationContext> future = exec.submit(new Callable<AbstractApplicationContext>(){

				public AbstractApplicationContext call()
						throws Exception {
					
					ClassLoader cl = createZipUnitClassLoad(basePath, 
							Thread.currentThread().getContextClassLoader());
					
					if (logger.isDebugEnabled()) {
						logger.debug( "try to set classloader: {}", cl );
					}
					
					//	change the current thread classloader
					Thread.currentThread().setContextClassLoader(cl);
					
					AbstractApplicationContext top = initTopContext(basePath);
					
					AbstractApplicationContext ctx = initZipUnitContext(top);
					
					if (logger.isDebugEnabled()) {
						logger.debug( "classloader setted: {}", Thread.currentThread().getContextClassLoader() );
					}
					
					return ctx;
				}});
			
			ret = future.get();
			
		}
    	catch (IOException e) {
			logger.error("failed to load zip unit {}, reason {}", source.getName(), e );
			return	null;
		} catch (Exception e) {
			logger.error("failed to load zip unit {}, reason {}", source.getName(), e );
			return	null;
		}

    	return ret;
    }

    /* (non-Javadoc)
     * @see com.skymobi.ebus.unit.EventUnit#destroy(java.lang.Object)
     */
//    @Override
    public void destroy(final Object ctx) {
    	if ( null != ctx && null != exec ) {
			Future<?> future = exec.submit(new Runnable() {

				public void run() {
		    		((AbstractApplicationContext)ctx).close();
				}});

			try {
				future.get();
			} catch (InterruptedException e) {
				logger.error("failed to destroy jar unit {}, reason {}", source.getName(), e );
			} catch (ExecutionException e) {
				logger.error("failed to destroy jar unit {}, reason {}", source.getName(), e );
			}
    	}
    	if ( null != exec ) {
    		exec.shutdownNow();
    		exec = null;
    	}
    }

    /* (non-Javadoc)
     * @see com.skymobi.ebus.unit.EventUnit#getDescription()
     */
//    @Override
    public String getDescription() {
        return source.getName();
    }

}
